/**
 * Copyright (c) 2022-2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "ecma_inlining.h"
#include "inline_call_intrinsics.h"
#include "events/events.h"
#include "optimizer/ir/graph.h"
#include "optimizer/analysis/loop_analyzer.h"
#include "optimizer/optimizations/cleanup.h"
#include "optimizer/optimizations/branch_elimination.h"
#include "optimizer/optimizations/peepholes.h"
#include "runtime/profiling/profiling.h"

namespace ark::compiler::ecmascript {

EcmaInlining::EcmaInlining(Graph *graph, uint32_t instructionsCount, uint32_t methodsInlined,
                           const ArenaVector<RuntimeInterface::MethodPtr> *inlinedStack)
    : Inlining(graph, instructionsCount, methodsInlined, inlinedStack),
      jsFunctions_(graph->GetLocalAllocator()->Adapter())
{
    instructionsLimit_ = g_options.GetCompilerEcmaInliningMaxInsts();
}

void EcmaInlining::RunOptimizations() const
{
    if (GetGraph()->RunPass<InlineCallIntrinsics>()) {
        GetGraph()->RunPass<LoopAnalyzer>();
    }
    if (GetGraph()->RunPass<Peepholes>()) {
        GetGraph()->RunPass<BranchElimination>();
    }
}

bool EcmaInlining::SkipBlock(const BasicBlock *block) const
{
    return Inlining::SkipBlock(block) || block->IsTry();
}

bool EcmaInlining::IsInstSuitableForInline(Inst *inst) const
{
    return inst->GetOpcode() == Opcode::CallDynamic;
}

bool EcmaInlining::ResolveTargets(CallInst *callInst)
{
    ASSERT(jsFunctions_.empty());
    auto runtime = GetGraph()->GetRuntime();
    auto kind =
        runtime->GetCallProfile(GetGraph()->GetMethod(), callInst->GetPc(), &jsFunctions_, GetGraph()->IsAotMode());
    if (kind == profiling::CallKind::MEGAMORPHIC || kind == profiling::CallKind::UNKNOWN) {
        LOG_INLINING(DEBUG) << "Call have " << CallKindToString(kind) << " type";
        EVENT_INLINE(runtime->GetMethodFullName(GetGraph()->GetMethod()), "-", callInst->GetId(),
                     events::InlineKind::DYNAMIC_POLYMORPHIC,
                     kind == profiling::CallKind::MEGAMORPHIC ? events::InlineResult::FAIL_MEGAMORPHIC
                                                              : events::InlineResult::FAIL_RESOLVE);
        return false;
    }
    return true;
}

bool EcmaInlining::IsInstSuitableForEcmaStaticInlining(Inst *defineFunc) const
{
    if (defineFunc->GetOpcode() != Opcode::Intrinsic) {
        return false;
    }
    switch (defineFunc->CastToIntrinsic()->GetIntrinsicId()) {
        case RuntimeInterface::IntrinsicId::INTRINSIC_DEFINEFUNC_DYN:
        case RuntimeInterface::IntrinsicId::INTRINSIC_DEFINE_NC_FUNC_DYN:
        case RuntimeInterface::IntrinsicId::INTRINSIC_DEFINE_METHOD:
            return true;
        default:
            return false;
    }
}

RuntimeInterface::MethodPtr EcmaInlining::TryResolveTargetStatic(CallInst *callInst) const
{
    auto defineFunc = callInst->GetDataFlowInput(0);
    if (IsInstSuitableForEcmaStaticInlining(defineFunc)) {
        auto loadFromCp = defineFunc->CastToIntrinsic()->GetInput(0).GetInst()->CastToLoadFromConstantPool();
        return GetGraph()->GetRuntime()->GetMethodByIdAndSaveJsFunction(GetGraph()->GetMethod(),
                                                                        loadFromCp->GetTypeId());
    }
    return nullptr;
}

bool EcmaInlining::CheckMethod(CallInst *callInst, InlineContext *ctx)
{
    if (!CheckMethodCanBeInlined<false>(callInst, ctx)) {
        return false;
    }

    if (!CheckBytecode(callInst, *ctx, nullptr)) {
        return false;
    }
    return true;
}

Graph *EcmaInlining::BuildGraph(InlineContext *ctx, CallInst *callInst, CallInst *newCallInst)
{
    auto graphInl = GetGraph()->CreateChildGraph(ctx->method);

    // Propagate instruction id counter to inlined graph, thereby avoid instructions id duplication
    graphInl->SetCurrentInstructionId(GetGraph()->GetCurrentInstructionId());
    graphInl->SetMaxInliningDepth(GetCurrentDepth());

    auto stats = GetGraph()->GetPassManager()->GetStatistics();
    auto savedPbcInstNum = stats->GetPbcInstNum();
    if (!TryBuildGraph(*ctx, graphInl, callInst, newCallInst)) {
        stats->SetPbcInstNum(savedPbcInstNum);
        return nullptr;
    }

    // Run basic optimizations
    graphInl->RunPass<InlineCallIntrinsics>();
    graphInl->RunPass<Cleanup>(false);
    if (graphInl->RunPass<Peepholes>()) {
        graphInl->RunPass<BranchElimination>();
        graphInl->RunPass<Cleanup>();
    }

    // Don't inline if we reach the limit of instructions and method is big enough.
    auto inlinedInstsCount = CalculateInstructionsCount(graphInl);
    if ((GetCurrentDepth() + 1U) < g_options.GetCompilerInliningMaxDepth()) {
        graphInl->RunPass<EcmaInlining>(instructionsCount_ + inlinedInstsCount, methodsInlined_ + 1U, &inlinedStack_);
    }
    instructionsCount_ += CalculateInstructionsCount(graphInl);
    GetGraph()->SetMaxMarkerIdx(graphInl->GetCurrentMarkerIdx());
    GetGraph()->SetCurrentInstructionId(graphInl->GetCurrentInstructionId());
    GetGraph()->SetMaxInliningDepth(graphInl->GetMaxInliningDepth() + 1);
    return graphInl;
}

void EcmaInlining::BuildGuard(CallInst *callInst, RuntimeInterface::MethodPtr methodPtr)
{
    auto graph = callInst->GetBasicBlock()->GetGraph();
    auto methodCnst = graph->CreateInstLoadImmediate(DataType::POINTER, callInst->GetPc(), methodPtr,
                                                     LoadImmediateInst::ObjectType::METHOD);
    auto loadMethod = graph->CreateInstLoadObject(DataType::POINTER, callInst->GetPc(), callInst->GetInput(0).GetInst(),
                                                  TypeIdMixin {TypeIdMixin::MEM_DYN_METHOD_ID, nullptr}, nullptr);
    loadMethod->SetObjectType(ObjectType::MEM_DYN_METHOD);
    auto cmpInst = graph->CreateInstCompare(DataType::BOOL, callInst->GetPc(), loadMethod, methodCnst,
                                            DataType::POINTER, ConditionCode::CC_NE);
    auto deoptInst =
        graph->CreateInstDeoptimizeIf(callInst->GetPc(), cmpInst, callInst->GetSaveState(), DeoptimizeType::INLINE_DYN);
    callInst->InsertBefore(methodCnst);
    callInst->InsertBefore(loadMethod);
    callInst->InsertBefore(cmpInst);
    callInst->InsertBefore(deoptInst);
}

void EcmaInlining::InsertGraph(CallInst *callInst, const InlineContext &ctx, Graph *graphInl)
{
    if (!IsStaticInlining()) {
        BuildGuard(callInst, ctx.method);
    }

    auto callBb = callInst->GetBasicBlock();
    auto callContBb = callBb->SplitBlockAfterInstruction(callInst, false);

    UpdateParameterDataflow(graphInl, callInst);
    UpdateDataflow(graphInl, callInst, callContBb);
    MoveConstants(graphInl);
    UpdateControlflow(graphInl, callBb, callContBb);

    if (callContBb->GetPredsBlocks().empty()) {
        GetGraph()->RemoveUnreachableBlocks();
    } else {
        returnBlocks_.push_back(callContBb);
    }

    bool needBarriers = GetGraph()->GetRuntime()->IsMemoryBarrierRequired(ctx.method);
    ProcessCallReturnInstructions(callInst, callContBb, true, needBarriers);

    callInst->SetCallMethod(ctx.method);
    callInst->SetCallMethodId(GetGraph()->GetRuntime()->GetMethodId(ctx.method));
    if (!IsStaticInlining()) {
        callInst->SetFunctionObject(jsFunctions_[0]);
    }
}

bool EcmaInlining::DoEcmaMonomorphicInilning(CallInst *callInst, InlineContext &ctx)
{
    ASSERT(jsFunctions_.size() <= 1U);
    LOG_INLINING(DEBUG) << "Try inline monomorphic" << (IsStaticInlining() ? "(static)" : "") << " CallDynamic:";
    LOG_INLINING(DEBUG) << "  instruction: " << *callInst;
    LOG_INLINING(DEBUG) << "Method: " << GetGraph()->GetRuntime()->GetMethodFullName(ctx.method, true);

    if (g_options.IsCompilerEcmaInlineBuiltins() && CheckInlinableBuiltin(callInst, ctx.method)) {
        // Actual inlining will happen during InlineIntrinsics:
        return false;
    }

    if (!CheckMethod(callInst, &ctx)) {
        LOG_INLINING(DEBUG) << "Unsuitable bytecode";
        return false;
    }

    // Build graph for current target and run EcmaInlining recursively
    auto graphInl = BuildGraph(&ctx, callInst);
    if (graphInl == nullptr) {
        return false;
    }
    InsertGraph(callInst, ctx, graphInl);
    GetGraph()->GetPassManager()->GetStatistics()->AddInlinedMethods(1);
    methodsInlined_++;
    EVENT_INLINE(GetGraph()->GetRuntime()->GetMethodFullName(GetGraph()->GetMethod()),
                 GetGraph()->GetRuntime()->GetMethodFullName(ctx.method), callInst->GetId(),
                 IsStaticInlining() ? events::InlineKind::STATIC : events::InlineKind::DYNAMIC_MONOMORPHIC,
                 events::InlineResult::SUCCESS);
    LOG_INLINING(DEBUG) << "Successfully monomorophic " << (IsStaticInlining() ? "(static)" : "")
                        << " inlined:" << GetGraph()->GetRuntime()->GetMethodFullName(ctx.method);
    return true;
}

void EcmaInlining::CreateCompareFunctions(CallInst *callInst, RuntimeInterface::MethodPtr methodPtr, BasicBlock *callBb)
{
    auto methodCnst = GetGraph()->CreateInstLoadImmediate(DataType::POINTER, callInst->GetPc(), methodPtr,
                                                          LoadImmediateInst::ObjectType::METHOD);
    auto loadMethod =
        GetGraph()->CreateInstLoadObject(DataType::POINTER, callInst->GetPc(), callInst->GetInput(0).GetInst(),
                                         TypeIdMixin {TypeIdMixin::MEM_DYN_METHOD_ID, nullptr}, nullptr);
    loadMethod->SetObjectType(ObjectType::MEM_DYN_METHOD);
    auto cmpInst = GetGraph()->CreateInstCompare(DataType::BOOL, callInst->GetPc(), loadMethod, methodCnst,
                                                 DataType::POINTER, ConditionCode::CC_EQ);
    auto ifInst = GetGraph()->CreateInstIfImm(DataType::BOOL, callInst->GetPc(), cmpInst, 0, DataType::BOOL,
                                              ConditionCode::CC_NE);
    callBb->AppendInst(methodCnst);
    callBb->AppendInst(loadMethod);
    callBb->AppendInst(cmpInst);
    callBb->AppendInst(ifInst);
}

bool EcmaInlining::DoEcmaPolymorphicInilning(CallInst *callInst)
{
    ASSERT(jsFunctions_.size() > 1U);
    ASSERT(jsFunctions_.size() <= profiling::MAX_FUNC_NUMBER);
    LOG_INLINING(DEBUG) << "Try inline polymorphic call(" << jsFunctions_.size() << " receivers):";
    LOG_INLINING(DEBUG) << "  instruction: " << *callInst;

    bool hasUnreachableBlocks = false;
    PhiInst *phiInst = nullptr;
    BasicBlock *callBb = nullptr;
    BasicBlock *callContBb = nullptr;
    auto runtime = GetGraph()->GetRuntime();
    auto inlinedMethods = methodsInlined_;
    for (uintptr_t func : jsFunctions_) {
        InlineContext ctx {runtime->GetMethodFromFunction(func)};
        LOG_INLINING(DEBUG) << "Method: " << GetGraph()->GetRuntime()->GetMethodFullName(ctx.method, true);
        if (!CheckMethod(callInst, &ctx)) {
            continue;
        }
        CallInst *newCallInst = callInst->Clone(GetGraph())->CastToCallDynamic();
        newCallInst->SetCallMethodId(runtime->GetMethodId(ctx.method));
        newCallInst->SetCallMethod(ctx.method);

        // Build graph for current target and run EcmaInlining recursively
        auto graphInl = BuildGraph(&ctx, callInst, newCallInst);
        if (graphInl == nullptr) {
            continue;
        }
        if (callBb == nullptr) {
            // Split block by call instruction
            callBb = callInst->GetBasicBlock();
            callContBb = callBb->SplitBlockAfterInstruction(callInst, false);
            if (callInst->GetType() != DataType::VOID) {
                phiInst = GetGraph()->CreateInstPhi(callInst->GetType(), callInst->GetPc());
                phiInst->ReserveInputs(jsFunctions_.size() << 1U);
                callContBb->AppendPhi(phiInst);
            }
        } else {
            auto newCallBb = GetGraph()->CreateEmptyBlock(callBb);
            callBb->GetLoop()->AppendBlock(newCallBb);
            callBb->AddSucc(newCallBb);
            callBb = newCallBb;
        }
        CreateCompareFunctions(callInst, ctx.method, callBb);

        // Create call_inlined_block
        auto callInlinedBlock = GetGraph()->CreateEmptyBlock(callBb);
        callBb->GetLoop()->AppendBlock(callInlinedBlock);
        callBb->AddSucc(callInlinedBlock);

        // Insert Call.inlined in call_inlined_block
        newCallInst->AppendInput(callInst->GetObjectInst());
        newCallInst->AppendInput(callInst->GetSaveState());
        newCallInst->SetInlined(true);
        newCallInst->SetFlag(inst_flags::NO_DST);
        newCallInst->SetFunctionObject(func);
        callInlinedBlock->PrependInst(newCallInst);

        // Create return_inlined_block and inster PHI for non void functions
        auto returnInlinedBlock = GetGraph()->CreateEmptyBlock(callBb);
        callBb->GetLoop()->AppendBlock(returnInlinedBlock);
        PhiInst *localPhiInst = nullptr;
        if (callInst->GetType() != DataType::VOID) {
            localPhiInst = GetGraph()->CreateInstPhi(callInst->GetType(), callInst->GetPc());
            localPhiInst->ReserveInputs(jsFunctions_.size());
            returnInlinedBlock->AppendPhi(localPhiInst);
        }

        // Inlined graph between call_inlined_block and return_inlined_block
        UpdateParameterDataflow(graphInl, callInst);
        UpdateDataflow(graphInl, callInst, localPhiInst, phiInst);
        MoveConstants(graphInl);
        UpdateControlflow(graphInl, callInlinedBlock, returnInlinedBlock);
        if (!returnInlinedBlock->GetPredsBlocks().empty()) {
            auto inlinedReturn = GetGraph()->CreateInstReturnInlined(DataType::VOID, INVALID_PC);
            returnInlinedBlock->PrependInst(inlinedReturn);
            inlinedReturn->SetInput(0, newCallInst->GetSaveState());
            if (callInst->GetType() != DataType::VOID) {
                ASSERT(phiInst);
                // clang-tidy think that phi_inst can be nullptr
                phiInst->AppendInput(localPhiInst);  // NOLINT
            }
            returnInlinedBlock->AddSucc(callContBb);
        } else {
            // We need remove return_inlined_block if inlined graph doesn't have Return inst(only Throw or Deoptimize)
            hasUnreachableBlocks = true;
        }
        GetGraph()->GetPassManager()->GetStatistics()->AddInlinedMethods(1);
        EVENT_INLINE(runtime->GetMethodFullName(GetGraph()->GetMethod()), runtime->GetMethodFullName(ctx.method),
                     callInst->GetId(), events::InlineKind::DYNAMIC_POLYMORPHIC, events::InlineResult::SUCCESS);
        LOG_INLINING(DEBUG) << "Successfully polymorphic inlined: " << GetMethodFullName(GetGraph(), ctx.method);
        methodsInlined_++;
    }
    if (callBb == nullptr) {
        // Nothing was inlined
        return false;
    }
    if (callContBb->GetPredsBlocks().empty() || hasUnreachableBlocks) {
        GetGraph()->RemoveUnreachableBlocks();
    }
    if (methodsInlined_ - inlinedMethods == jsFunctions_.size()) {
        InsertDeoptimizeInst(callInst, callBb, DeoptimizeType::INLINE_DYN);
    } else {
        InsertCallInst(callInst, callBb, callContBb, phiInst);
    }

    if (callInst->GetType() != DataType::VOID) {
        callInst->ReplaceUsers(phiInst);
    }

    ProcessCallReturnInstructions(callInst, callContBb, true);
    callInst->GetBasicBlock()->RemoveInst(callInst);
    return true;
}

bool EcmaInlining::TryInline(CallInst *callInst)
{
    jsFunctions_.clear();
    auto method = TryResolveTargetStatic(callInst);
    if (method != nullptr) {
        InlineContext ctx {method};
        return DoEcmaMonomorphicInilning(callInst, ctx);
    }
    if (!ResolveTargets(callInst)) {
        LOG_INLINING(DEBUG) << "Target methods were not found for CallDynaimc(id = " << callInst->GetId() << ")";
        return false;
    }
    ASSERT(!jsFunctions_.empty());
    if (IsMonomorphicInlining()) {
        InlineContext ctx {GetGraph()->GetRuntime()->GetMethodFromFunction(jsFunctions_[0])};
        return DoEcmaMonomorphicInilning(callInst, ctx);
    }
    return DoEcmaPolymorphicInilning(callInst);
}

bool EcmaInlining::CheckInlinableBuiltin(CallInst *callInst, RuntimeInterface::MethodPtr method)
{
    if (GetGraph()->GetRuntime()->IsInlinableNativeMethod(method)) {
        callInst->SetCallMethod(method);
        ASSERT(!jsFunctions_.empty());
        return true;
    }
    return false;
}

}  // namespace ark::compiler::ecmascript
